<html>
    <head>
        <title>EF Core 数据库迁移</title>
    </head>
    <body>
        <script>
            //  EF Core 数据库迁移

                    /*
                        一、关于 EF Core  的 Code First 迁移

                                  Entity Framework Core提供了一个易于使用且功能强大的数据库迁移系统。ABP启动模板利用此系统允许你以标准方式开发应用程序。

                                  然而，EF Core迁移系统在模块化环境中不太好，其中每个模块维护其自己的数据库模式，而实际上两个或多个模块可能共享单个数据库。

                                  由于ABP在各个方面都关心模块化，因此它为这个问题提供了一个解决方案。

                                  如果你需要自定义你的数据库结构，了解此解决方案很重要。

                    */

                    /*
                         二、默认解决方案 和 数据库配置

                                当你创建一个新的Web应用程序（使用EF Core，这是默认的数据库提供程序）时，你的解决方案结构将类似于下图：

                                        《图片略，就是标准的分层解决方案》

                                1、数据库结构

                                                启动模板预装了一些应用程序模块。解决方案的每一层都有相应的模块包引用。
                                                
                                                因此，.EntityFrameworkCore项目具有.EntityFrameworkCore使用模块的包：

                                                《图片略，就是基础层默认的包引用》

                                                通过这种方式，你可以收集.EntityFrameworkCore项目下的所有关于EF Core的依赖包。 

                                                虽然每个模块在设计上都有自己的DbContext类，并且可以使用其自己的物理数据库，

                                                但解决方案配置为使用单个共享数据库，如下图所示：

                                                《图片略，大体意思是每个模块都有单独的DbContext，虽然可以拥有自己的独立数据库，但是默认分层解决方案中配置为每个模块共同使用默认数据库》

                                                这是最简单的配置，适用于大多数应用程序。
                                                
                                                appsettings.json文件有一个单连接字符串，名为Default：

                                                        "ConnectionStrings": {
                                                            "Default": "..."
                                                        }

                                                因此，你有一个单个数据库模式，其中包含共享此数据库的模块的所有表。

                                                ABP的连接字符串系统允许你轻松地为所需模块设置不同的连接字符串：

                                                    "ConnectionStrings": {
                                                        "Default": "...",
                                                        "AbpAuditLogging": "..."
                                                    }
                                                    
                                                    示例配置告诉ABP使用审核日志模块的第二个连接字符串（如果你没有为模块指定连接字符串，它会使用Default连接字符串）。

                                                但是，只有当具有给定连接字符串的审计日志数据库可用时，这才能工作。

                                                因此，你需要创建第二个数据库，在其中创建审计日志表并维护数据库表。

                                                如果你手动执行所有这些操作，没有问题。但是，推荐的方法是代码优先迁移。本文档的主要目的之一是指导你了解此类数据库分离场景

                                                
                                                ★、模块表

                                                        每个模块都使用其自己的数据库表。例如，Identity Module有一些表来管理系统中的用户和角色。

                                                        ①、表前缀

                                                                由于允许所有模块共享单个数据库（这是默认配置），因此模块通常使用表名前缀对其自己的表进行分组。

                                                                基本模块，如Identity、租户管理和审核日志，使用Abp前缀，而其他一些模块使用自己的前缀。

                                                                Identity Server模块使用IdentityServer前缀。

                                                                如果需要，你可以更改应用程序模块的数据库表名前缀。
                                                                
                                                                例子：

                                                                    Volo.Abp.IdentityServer.AbpIdentityServerDbProperties.DbTablePrefix = "Ids";

                                                                此代码更改了Identity Server模块的前缀。在应用程序的开头编写此代码。


                                                                （每个模块还定义了DbSchema属性（靠近DbTablePrefix），因此你可以为支持模式使用的数据库设置它。）


                                                

                                2、.EntityFrameworkCore 项目

                                        该解决方案包含一个项目，其名称以.EntityFrameworkCore。

                                        此项目具有应用程序的DbContext类（BookStoreDbContext对于此示例）。

                                        每个模块都使用自己的DbContext类来访问数据库。同样，你的应用程序有自己的DbContext。

                                        你通常在应用程序代码中使用此DbContext（如果你遵循最佳实践并将数据访问代码隐藏在存储库后面，则在你的存储库中）。

                                        它几乎是一个空的DbContext，因为你的应用程序一开始没有任何实体：

                                                        [ReplaceDbContext(typeof(IIdentityDbContext))]  // 替换IIdentityDbContext的默认实现
                                                        [ReplaceDbContext(typeof(ITenantManagementDbContext))]  // 替换ITenantManagementDbContext的默认实现
                                                        [ConnectionStringName("Default")]
                                                        public class BookStoreDbContext :
                                                            AbpDbContext<BookStoreDbContext>,
                                                            IIdentityDbContext,
                                                            ITenantManagementDbContext
                                                        {
                                                            //Add DbSet properties for your Aggregate Roots / Entities here. 
                                                                
                                                            // DbSet for entities from the replaced DbContexts 

                                                            public BookStoreDbContext(DbContextOptions<BookStoreDbContext> options)
                                                                : base(options)
                                                            {

                                                            }

                                                            protected override void OnModelCreating(ModelBuilder builder)
                                                            {
                                                                base.OnModelCreating(builder);
                                                                
                                                                //* Include modules to your migration db context 
                                                                builder.ConfigurePermissionManagement();
                                                                builder.ConfigureSettingManagement();
                                                                builder.ConfigureBackgroundJobs();
                                                                builder.ConfigureAuditLogging();
                                                                builder.ConfigureIdentity();
                                                                builder.ConfigureIdentityServer();
                                                                builder.ConfigureFeatureManagement();
                                                                builder.ConfigureTenantManagement();

                                                                // Configure your own tables/entities here. Example:       
                                                                //builder.Entity<YourEntity>(b =>
                                                                //{
                                                                //    b.ToTable("YourEntities");
                                                                //    b.ConfigureByConvention(); //auto configure for the base properties
                                                                //    //...
                                                                //});
                                                            }
                                                        }
                                        这个DbContext类需要一些解释：

                                            ①、它使用[ReplaceDbContext]特性，替换了IIDentityDbContext、ITenantManagementDbContext的实现

                                                    这些属性在运行时用你的DbContext替换Identity和 TenantManagenment 模块的DbContext。
                                                    
                                                    这允许我们通过将你的实体与来自这些模块的实体（通过存储库）连接来轻松执行LINQ查询。

                                            ②、它定义了一个[ConnectionStringName]属性，告诉ABP始终为此Dbcontext使用Default连接字符串。

                                            ③、它继承自AbpDbContext<T>而不是标准的DbContext类。

                                                    你可以查看EF Core集成文档了解更多信息。现在，知道AbpDbContext<T>基类实现了ABP的一些约定，为你自动执行一些常见任务。

                                            ④、它为替换的DbContexts中的实体声明了DbSet属性（通过实现相应的接口）。

                                                      这些DbSet属性未在上面显示（为简洁起见），但你可以在region中的应用程序代码中找到。
                                                      
                                            ⑤、构造函数采用DbContextOptions<T>实例，并将其传递给基类。

                                            ⑥、它覆盖OnModelCreating方法来定义EF Core映射。
                                                                    
                                                        它首先调用base.OnModelCreating方法让ABP为我们实现基础的映射。

                                                        然后它为使用的模块调用一些builder.ConfigureXXX()方法。

                                                        这使得将这些模块的数据库映射添加到此DbContext成为可能，因此当我们添加新的EF Core数据库迁移时，它会创建模块的数据库表。

                                                        你可以按照示例代码中的注释为自己的实体配置映射。此时，你还可以更改正在使用的模块的映射。


                    */

                    /*
                        三、使用多个数据库

                               默认的启动模板被设计成所有模块和你的应用程序都使用用一个数据库。
                               
                               但是，ABP和所有预构建的模块都被设计为可以使用多个数据库。

                               每个模块可以使用自己的数据库，或者你可以将模块分组到几个数据库中。

                               本节将解释如何将审核日志记录、设置管理和权限管理模块表移动到第二个数据库，而其余模块继续使用主（“默认”）数据库。


                                        1、更改连接字符串
                                         
                                                第一步是更改所有appsettings.json文件中的连接字符串部分。最初，它是这样的：

                                                    "ConnectionStrings": {
                                                        "Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=BookStore;Trusted_Connection=True"
                                                    }

                                                更改如下所示：

                                                    "ConnectionStrings": {
                                                        "Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=BookStore;Trusted_Connection=True",
                                                        "AbpPermissionManagement": "Server=(LocalDb)\\MSSQLLocalDB;Database=BookStore_SecondDb;Trusted_Connection=True",
                                                        "AbpSettingManagement": "Server=(LocalDb)\\MSSQLLocalDB;Database=BookStore_SecondDb;Trusted_Connection=True",
                                                        "AbpAuditLogging": "Server=(LocalDb)\\MSSQLLocalDB;Database=BookStore_SecondDb;Trusted_Connection=True"
                                                    }

                                                为相关模块多添加了三个连接字符串，以针对BookStore_SecondDb数据库（它们都是相同的）。

                                                例如，AbpPermissionManagement是权限管理模块使用的连接字符串名称。

                                                "AbpPermissionManagement" 是权限管理模块定义的常量.  
                                                
                                                如果你在appsettings.json文件中这样定义，ABP连接字符串选择系统会为权限管理模块选择此连接字符串。

                                                如果你不定义，它会回退到Default连接字符串。



                                        2、创建第二个DbContext

                                                在运行时定义上面解释的连接字符串就足够了.  但是，BookStore_SecondDb数据库还不存在。你需要为相关模块创建数据库和表。

                                                就像主数据库一样，我们希望使用EF Core Code First迁移系统来创建和维护第二个数据库。

                                                因此，在.EntityFrameworkCore项目创建DbContext类：

                                                        using Microsoft.EntityFrameworkCore;
                                                        using Volo.Abp.AuditLogging.EntityFrameworkCore;
                                                        using Volo.Abp.Data;
                                                        using Volo.Abp.EntityFrameworkCore;
                                                        using Volo.Abp.PermissionManagement.EntityFrameworkCore;
                                                        using Volo.Abp.SettingManagement.EntityFrameworkCore;

                                                        namespace BookStore.EntityFrameworkCore
                                                        {
                                                            [ConnectionStringName("AbpPermissionManagement")]  // 设置连接字符串
                                                            public class BookStoreSecondDbContext :
                                                                AbpDbContext<BookStoreSecondDbContext>
                                                            {
                                                                public BookStoreSecondDbContext(
                                                                    DbContextOptions<BookStoreSecondDbContext> options)
                                                                    : base(options)
                                                                {
                                                                }

                                                                protected override void OnModelCreating(ModelBuilder builder)
                                                                {
                                                                    base.OnModelCreating(builder);

                                                                    // 将依赖模块的DbContext配置到当前DbContext中
                                                                    builder.ConfigurePermissionManagement();
                                                                    builder.ConfigureSettingManagement();
                                                                    builder.ConfigureAuditLogging();
                                                                }
                                                            }
                                                        }

                                                注解： [ConnectionStringName(...)]属性在这里很重要，它告诉ABP应该为此DbContext使用哪个连接字符串。
                                                        
                                                       我们已经使用了AbpPermissionManagement，但都是一样的。

                                                我们需要将这个BookStoreSecondDbContext类注册到依赖注入系统中。

                                                在BookStore.EntityFrameworkCore项目中打开BookStoreEntityFrameworkCoreModule类，并将以下行添加到ConfigureServices方法中：

                                                        context.Services.AddAbpDbContext<BookStoreSecondDbContext>();


                                                我们还应该创建一个Design Time Db Factory类，由EF Core工具使用（例如，通过Add-Migration和Update-DatabasePCM命令）：

                                                        using System.IO;
                                                        using Microsoft.EntityFrameworkCore;
                                                        using Microsoft.EntityFrameworkCore.Design;
                                                        using Microsoft.Extensions.Configuration;

                                                        namespace BookStore.EntityFrameworkCore
                                                        {
                                                            // This class is needed for EF Core console commands
                                                            // (like Add-Migration and Update-Database commands) 
                                                            public class BookStoreSecondDbContextFactory
                                                                : IDesignTimeDbContextFactory<BookStoreSecondDbContext>
                                                            {
                                                                public BookStoreSecondDbContext CreateDbContext(string[] args)
                                                                {
                                                                    var configuration = BuildConfiguration();
                                                                    var builder = new DbContextOptionsBuilder<BookStoreSecondDbContext>()
                                                                        .UseSqlServer(configuration.GetConnectionString("AbpPermissionManagement"));
                                                                    return new BookStoreSecondDbContext(builder.Options);
                                                                }

                                                                private static IConfigurationRoot BuildConfiguration()
                                                                {
                                                                    var builder = new ConfigurationBuilder()
                                                                        .SetBasePath(Path.Combine(Directory.GetCurrentDirectory(), "../BookStore.DbMigrator/"))
                                                                        .AddJsonFile("appsettings.json", optional: false);

                                                                    return builder.Build();
                                                                }
                                                            }
                                                        }

                                                现在，你可以打开包管理器控制台，选择.EntityFrameworkCore项目作为默认项目（确保.Web项目仍然是启动项目）并运行以下命令：

                                                        " Add-Migration "Initial" -OutputDir "SecondDbMigrations" -Context BookStoreSecondDbContext "

                                                这将在.EntityFrameworkCore项目中添加一个 "SecondDbMigrations文件夹" 和其中的迁移类。

                                                OutputDir和Context参数是必需的，因为我们目前在同一个项目中有两个DbContext类和两个迁移文件夹。

                                                你现在可以运行以下命令来创建数据库及其中的表：

                                                        "Update-Database -Context BookStoreSecondDbContext"

                                                应该创建一个名为 " BookStore_SecondDb " 的新数据库。


                                    3、从主数据库中删除模块

                                            我们已经创建了第二个数据库，其中包含审计日志记录、权限管理和设置管理模块的表。

                                            因此，我们应该从主数据库中删除这些表。这很容易。

                                            首先，从BookStoreDbContext类中删除以下行：

                                                    builder.ConfigurePermissionManagement();
                                                    builder.ConfigureSettingManagement();
                                                    builder.ConfigureAuditLogging();   
                                                    
                                            打开包管理器控制台，选择.EntityFrameworkCore作为默认项目（确保.Web项目仍然是启动项目）并运行以下命令：

                                                    " Add-Migration "Removed_Audit_Setting_Permission_Modules" -Context BookStoreDbContext " 
                                                        
                                            此命令将创建一个新的迁移类，如下所示：

                                                    public partial class Removed_Audit_Setting_Permission_Modules : Migration
                                                    {
                                                        protected override void Up(MigrationBuilder migrationBuilder)
                                                        {
                                                            migrationBuilder.DropTable(
                                                                name: "AbpAuditLogActions");

                                                            migrationBuilder.DropTable(
                                                                name: "AbpEntityPropertyChanges");

                                                            migrationBuilder.DropTable(
                                                                name: "AbpPermissionGrants");

                                                            migrationBuilder.DropTable(
                                                                name: "AbpSettings");

                                                            migrationBuilder.DropTable(
                                                                name: "AbpEntityChanges");

                                                            migrationBuilder.DropTable(
                                                                name: "AbpAuditLogs");
                                                        }

                                                        ...
                                                    }

                                             在这一步中要小心：

                                                    如果你有一个live system，那么你应该关心数据丢失。在删除表之前，你需要将表内容移动到第二个数据库。
                                                    
                                                    如果你尚未启动你的项目，你可以考虑删除所有迁移并重新创建初始迁移以获得更清晰的迁移历史记录。

                                            运行以下命令从主数据库中删除表：

                                                    "Update-Database -Context BookStoreDbContext"


                                            ★、注意： 请注意，如果你没有将初始种子数据复制到新数据库，你还删除了一些初始种子数据（例如，管理员角色的权限授予）。
                                                      如果你运行应用程序，你可能不再登录。
                                                      解决方案很简单：重新运行.DbMigrator控制台应用程序在你的解决方案中，它将为新数据库播种。

                                    

                                    4、让第二数据库的迁移实现自动化  

                                                .DbMigrator控制台应用程序可以跨多个数据库运行数据库种子代码,，无需任何额外配置。

                                                但是，它不能为BookStoreSecondDbContext的数据库应用EF Core Code First Migrations。

                                                现在，你将看到如何配置控制台迁移应用程序来处理这两个数据库。

                                                EntityFrameworkCoreBookStoreDbSchemaMigrator是在Acme.BookStore.EntityFrameworkCore项目中的类，
                                                
                                                负责迁移BookStoreMigrationsDbContext的数据库模式。应该是这样的：

                                                            using System;
                                                            using System.Threading.Tasks;
                                                            using Microsoft.EntityFrameworkCore;
                                                            using Microsoft.Extensions.DependencyInjection;
                                                            using BookStore.Data;
                                                            using Volo.Abp.DependencyInjection;

                                                            namespace BookStore.EntityFrameworkCore
                                                            {
                                                                public class EntityFrameworkCoreBookStoreDbSchemaMigrator
                                                                    : IBookStoreDbSchemaMigrator, ITransientDependency
                                                                {
                                                                    private readonly IServiceProvider _serviceProvider;

                                                                    public EntityFrameworkCoreBookStoreDbSchemaMigrator(
                                                                        IServiceProvider serviceProvider)
                                                                    {
                                                                        _serviceProvider = serviceProvider;
                                                                    }

                                                                    public async Task MigrateAsync()
                                                                    {
                                                                        // We intentionally resolving the BookStoreDbContext
                                                                        // from IServiceProvider (instead of directly injecting it)
                                                                        // to properly get the connection string of the current tenant in the
                                                                        // current scope.

                                                                        await _serviceProvider
                                                                            .GetRequiredService<BookStoreDbContext>()
                                                                            .Database
                                                                            .MigrateAsync();
                                                                    }
                                                                }
                                                            }

                                                在MigrateAsync方法中添加以下代码：

                                                            await _serviceProvider
                                                                                .GetRequiredService<BookStoreSecondDbContext>()
                                                                                .Database
                                                                                .MigrateAsync();

                                                因此，MigrateAsync方法应如下所示：
                                                                            
                                                            public async Task MigrateAsync()
                                                            {
                                                                // We intentionally resolving the BookStoreDbContext
                                                                    * from IServiceProvider (instead of directly injecting it)
                                                                    * to properly get the connection string of the current tenant in the
                                                                    * current scope.

                                                                await _serviceProvider
                                                                    .GetRequiredService<BookStoreDbContext>()
                                                                    .Database
                                                                    .MigrateAsync();

                                                                await _serviceProvider
                                                                    .GetRequiredService<BookStoreSecondDbContext>()
                                                                    .Database
                                                                    .MigrateAsync();
                                                            }

                                                就是这么简单。你现在可以运行.DbMigrator应用程序来迁移和播种数据库。

                                                要进行测试话，可以删除两个数据库并再次运行.DbMigrator应用程序以查看它是否创建了两个数据库。




                                   5、修复测试  Fixing the Tests 
                                   
                                            创建新的DbContext会破坏集成测试。很容易修复。

                                            在BookStore.EntityFrameworkCore.Tests项目中打开BookStoreEntityFrameworkCoreTestModule类，找到CreateDatabaseAndGetConnection方法。
                                            
                                            应该是这样的：
                                                                        
                                                    private static SqliteConnection CreateDatabaseAndGetConnection()
                                                    {
                                                        var connection = new SqliteConnection("Data Source=:memory:");
                                                        connection.Open();

                                                        var options = new DbContextOptionsBuilder<BookStoreDbContext>()
                                                            .UseSqlite(connection)
                                                            .Options;

                                                        using (var context = new BookStoreDbContext(options))
                                                        {
                                                            context.GetService<IRelationalDatabaseCreator>().CreateTables();
                                                        }

                                                        return connection;
                                                    }
                                            
                                            将其更改为以下内容：

                                                    private static SqliteConnection CreateDatabaseAndGetConnection()
                                                    {
                                                        var connection = new SqliteConnection("Data Source=:memory:");
                                                        connection.Open();

                                                        var options = new DbContextOptionsBuilder<BookStoreDbContext>()
                                                            .UseSqlite(connection)
                                                            .Options;

                                                        using (var context = new BookStoreDbContext(options))
                                                        {
                                                            context.GetService<IRelationalDatabaseCreator>().CreateTables();
                                                        }

                                                        // Add the following code --------------
                                                        var optionsForSecondDb = new DbContextOptionsBuilder<BookStoreSecondDbContext>()
                                                            .UseSqlite(connection)
                                                            .Options;

                                                        using (var context = new BookStoreSecondDbContext(optionsForSecondDb))
                                                        {
                                                            context.GetService<IRelationalDatabaseCreator>().CreateTables();
                                                        }
                                                        //--------------------------------------

                                                        return connection;
                                                    }    
                                                        
                                        集成测试现在可以工作了。我在测试中使用了相同的数据库以保持简单。
                    */


                    /*
                        四、分离主机和租户数据库

                                在多租户解决方案中，你可能希望分离数据库模式，因此当租户拥有单独的数据库时，与主机相关的表不会位于租户数据库中。

                                一些预构建的ABP模块仅与主机端相关，
                                
                                例如租户管理模块。因此，在租户DbContext类中，你不会调用modelBuilder.ConfigureTenantManagement()仅此而已。



                                有些模块，如Identity模块，既用于主机端，也用于租户端。它将租户用户存储在租户数据库中，将主机用户存储在主机数据库中。

                                但是，它将一些实体，如IdentityClaimType，仅存储在主机端。

                                在这种情况下，你不希望在租户数据库中添加这些表，即使它们没有被使用并且对于租户来说总是为空。

                                ABP提供了一种为DbContext设置多租户端的简单方法，因此模块可以检查它并决定是否将表映射到数据库。

                                                public class MyTenantDbContext : AbpDbContext<MyTenantDbContext>
                                                {
                                                    protected override void OnModelCreating(ModelBuilder modelBuilder)
                                                    {
                                                        modelBuilder.SetMultiTenancySide(MultiTenancySides.Tenant); // 将当前DbContext 设置为租户端数据库
                                                        
                                                        base.OnModelCreating(modelBuilder);
                                                        
                                                        modelBuilder.ConfigureIdentity(); // 此模块中有租户端表，也有主机端表，只会在此数据库创建租户端表
                                                        modelBuilder.ConfigureFeatureManagement();  // 此模块所有的表都是主机端表，因此不会创建它们，即使调用此方法也不会有任何作用
                                                        modelBuilder.ConfigureAuditLogging();
                                                    }
                                                }

                                            OnModelCreating中的第一行将多租户端设置为Tenant。

                                            对于此示例，不会创建功能管理表（因为所有表都是特定于主机的），因此调用modelBuilder.ConfigureFeatureManagement()没有效果。

                                            ConfigureIdentity() 只会创建IDentity模块的租户端表。


                                           ★、etMultiTenancySide可以得到以下值：

                                                    ①、MultiTenancySides.Both（默认值）：此DbContext（以及相关数据库）由主机和租户共享、

                                                    ②、MultiTenancySides.Host：此DbContext（以及相关数据库）仅由主机端使用。

                                                    ③、MultiTenancySides.Tenant：此DbContext（以及相关数据库）仅适用于租户。




                                            如果你创建了一个可重用的应用程序模块或想要在应用程序代码中检查该值，
                                            
                                            你可以使用modelBuilder.GetMultiTenancySide()检查当前端（主机还是租户）：

                                                    var side = modelBuilder.GetMultiTenancySide();
                                                    if (!side.HasFlag(MultiTenancySides.Host))
                                                    {
                                                        ...
                                                    }
                                            或者实际上你可以使用快捷方式扩展方法之一：

                                                    if (modelBuilder.IsTenantOnlyDatabase())
                                                    {
                                                        ...
                                                    }

                                            ★、有四种方法可以检查当前侧：

                                                    ①、IsHostDatabase()：如果你应该创建与主机相关的表，则返回true。
                                                    
                                                                这相当于检查：  modelBuilder.GetMultiTenancySide().HasFlag(MultiTenancySides.Host)

                                                    ②、IsHostOnlyDatabase()：如果你应该只创建与主机相关的表，但不应该创建与租户相关的表，则返回true。
                                                    
                                                                这相当于检查modelBuilder.GetMultiTenancySide() == MultiTenancySides.Host。

                                                    ③、IsTenantDatabase()：如果你应该创建与租户相关的表，则返回true。
                                                    
                                                                这相当于检查modelBuilder.GetMultiTenancySide().HasFlag(MultiTenancySides.Tenant)。

                                                    ④、IsTenantOnlyDatabase()：如果你应该只创建与租户相关的表，但不应该创建与主机相关的表，则返回true。
                                                        
                                                                这相当于检查modelBuilder.GetMultiTenancySide() == MultiTenancySides.Tenant。

                    */
        </script>
    </body>
</html>